Skip to content

feat(routes): CLI surface projections of MCP tools — colocated <tool>.cli.ts, mapInput, AB4843–AB4845 (#596) - #616

Merged
ScriptedAlchemy merged 33 commits into
mainfrom
feat/596-cli-surface-projection
Sep 5, 2026
Merged

feat(routes): CLI surface projections of MCP tools — colocated <tool>.cli.ts, mapInput, AB4843–AB4845 (#596)#616
ScriptedAlchemy merged 33 commits into
mainfrom
feat/596-cli-surface-projection

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Closes #596. Builds on #593 (#603). Design comment (posted before implementation): #596 (comment).

What changes

One operation, two surface projections. The MCP tool module stays the only executable route (tool:<server>/<tool>: execution, inputSchemaRouteContract, resultSchema, domain errors, rendered document). Its MCP surface projection is the tool config as today. A new opt-in CLI surface projection is a colocated module that is never a route:

src/mcp/hauler/tools/hauler_request.tsx      # the operation
src/mcp/hauler/tools/hauler_request.cli.ts   # its CLI surface projection
import type { CliProjectionConfig } from 'agent-bundle/routes';
import type { inputSchema } from './hauler_request.js';

export const config = {
  command: ['request'],                       // default [tool]
  confirm: false,                             // default !(annotations.readOnlyHint === true)
  description: '…',
  flags: {
    after:   { description: '… (repeatable, or comma-separated)' },
    cwd:     { description: 'Workspace directory (default: current directory)', required: false },
    host:    { description: 'Agent host name for attribution' },
    session: { description: 'Agent session id for attribution' },
  },
  positionals: ['argv'],
} satisfies CliProjectionConfig<typeof inputSchema>;

export const mapInput = (input: CliInput): z.input<typeof inputSchema> => ({
  ...input, cwd: input.cwd ?? process.cwd(), ...(after.length === 0 ? {} : { after }),
});

Vocabulary follows #578/#592: host projection = targets (claude/codex/cursor/portable); surface projection = MCP tool vs CLI command (the code's existing surface axis: AgentInvocationKind, CompiledCliSurface). The two compose orthogonally; the routed CLI bin is still emitted once per composite root (AB4765/AB4766 unchanged).

Compiler (routes/cli-projection.ts new leaf; graph.ts, cli-commands.ts, cli-argv.ts). Discovery excludes src/mcp/*/tools/*.cli.{ts,tsx} from route classification before identity derivation (today such a file becomes the broken tool tool:<server>/<tool>.cli, AB4810) and pairs it with the sibling tool; _-prefixed parks it. config is read by the unchanged static grammar (extractRouteConfig: literals + string consts via the #593 resolver; satisfies unwraps); mapInput presence is recorded from scanRouteModuleExports().namedFunctions. compileProjectedCliCommands produces a CompiledCliCommand with routeId = the tool id, mcp: { server, tool, confirm } provenance, path = command ?? [tool], and options from the tool's RouteContract.input through the same cliOptionFor policy as src/cli routes with a per-key override (name, aliases, description, default, required: false) applied inside the policy so kebab/reserved/collision checks run on final spellings and aliases (yes reserved when confirming). A tool with a projection leaves the bulk routes.mcpCommands set (one command per operation; an include matching only such tools is AB4822 naming the module). AB4813 collisions cover projected commands with a projection recovery. A tool without a static contract but with a projection is judged like a CLI route: AB4814/AB4838/AB4839 fire with the prefix Tool route <path> (CLI projection <module>). AB4837 framework-import judgement applies to the projection module. safeIdentitySegment is now one shared export (two private copies removed).

Projection IR (routes/types.ts, additive; inspect --routes dumps it unchanged):

CompiledCliOption.aliases?: readonly string[]
CompiledCliProjection { mapInput: boolean; module: string /* project-relative */; defaults?: Record<key, scalar | scalar[]>; relaxed?: readonly string[] }
CompiledCliCommand.projection?: CompiledCliProjection
CompiledCliSurface.projectionSources?: Record<routeId, absolutePath>   // build-side, outside the digest identity

options is the mapping ({ key: 'laneKey', option: 'lane' }, { key: 'tickets', option: 'ticket', repeated: true }, { key: 'argv', positional: 0, repeated: true }). defaults holds only flags.<key>.default values; canonical Zod .default()s stay in CompiledCliOption.defaultValue for help text and are applied by inputSchema.parse, never before mapInput. The route manifest (RouteManifestCliCommand.projection, RouteManifestCliOption.aliases, RouteManifestCliProjection exported from agent-bundle/dev and the browser contracts) and the Workbench strict decoder mirror the fields and are tested; the old Workbench Routes page is not enriched (#600 is dismantling it) — projection details surface later in the selected-operation inspector of the Application explorer, and a comment in routes-page.tsx says so. #592 manifest v2 reserved routes.cli.commands[].projection; the name is kept. NormalizedBinEntry.generatedCli.projectionSources carries the sources to the bin build.

Runtime (build/entry-shell.ts, cli-entry.ts, build/cli-bins.ts, build/package-build.ts, test/cli.ts, test/render.ts). The generated bin (npm dist/bin/<name>.js and artifact bin/<name>.mjs, one template) imports each projection module beside its route module. The public shell's parseMcpCommandInput (cli-entry.ts) is the single confirmation guard for every command with mcp.confirm, bulk or projected: no --yesCliUsageError (exit 2) before any callback runs, and yes is stripped. Then parseInput: (1) fills projection.defaults for absent keys — only those; (2) calls mapInput synchronously — a throw is a CliInputError (exit 2), a non-function a TypeError; (3) validates the mapped input with the canonical inputSchema (issues spelled --<option>), which is where canonical Zod defaults apply. Every command the CLI executable dispatches — src/cli route, explicit projection, and the bulk mcpCommands projection — runs the route with invocation: { kind: 'cli', operationId: 'tool:<server>/<tool>', surface: '<path>' } and providers { kind: 'cli', props: { args, command } }; the generated MCP server still passes kind: 'tool'. Both projection mechanisms are CLI surfaces, so a route never observes a different kind depending on which produced the command (owner requirement; previously the bulk path passed kind: 'tool'). The worker route table (route.kind) is untouched, so layouts still wrap. Help prints MCP tool: <server>:<tool> and Projection: <module> and lists aliases. The two duplicated bin sourceInputs lists are one cliBinSourceInputs helper that includes projection sources; the projection module is bundled into the bin only, never the Flight worker or MCP entry. The test harness (invokeCli/render) loads projection modules through the generated Rstest registry like routes, layouts, and providers (rstest/setup-module.ts emits projectionLoaders as static import()s, registry version 7; loadCliProjectionModule resolves lazily, file-URL import only without a registry, failures are AgentTestError) so .cli.tsx and .js-specifier value imports work under Rstest's module graph; it applies the identical steps and parity is pinned in entry-shell.test.ts.

Not forced, not leaked. No projection module → no command (bulk mcpCommands unchanged for the rest). ToolConfig, tools/list, annotations, _meta gain nothing.

Diagnostics (docs/diagnostics.md; the reference page renders from it)

Codes are AB4843AB4845: #618 (event preflight gates, #595) landed first and holds AB4840/AB4841, so the projection codes moved up by three on the merge (1297c46dc); the design comment and earlier review threads say AB4840AB4842 for the same three diagnostics.

Code Severity Trigger
AB4843 error .cli.{ts,tsx} under src/mcp/<server>/tools/ with no sibling tool (orphan), under resources//prompts//apps/, or a second projection module for the same tool. Recovery: rename to match the tool or prefix _. Reserved under src/mcp/** only.
AB4844 error Projection contract: config missing/outside the static grammar (includes the AB4805/AB4806 reason), a key outside the closed set, wrong field shape, required: false/default on a canonical-required key without mapInput; mapInput that cannot be a runtime synchronous mapper — ambient declare function/declare const, generator, async generator, async function, a re-export the scan cannot follow, or not statically a function (a relative export { mapInput } from './x' is followed and judged at its declaration; overloads accepted).
AB4845 error Grammar binding: flags/positionals name a key absent from the tool's contract; a name/alias is not kebab-case, reserved (help/json/ndjson/version), or collides; a canonical key named yes on a confirming tool (whatever its name); name/aliases on a positional key; a command segment is not a safe identity segment.

Message shape CLI projection <module> for tool:<server>/<tool>: <detail>., sourcePath = the projection module. All errors: a projection that cannot compile has no correct partial output. AB4804 also names projection modules when routes.cli: 'conventional'; AB4814/AB4838/AB4839 gain the relabelled tool-route prefix.

Tests

  • Unit tests/cli-projection.test.ts (27 cases): pairing without a route/contract binding; default command/aliases; renamed/repeated/positional/defaulted/relaxed options; confirmation + metadata defaults; AB4843 orphan/misplaced/parked; AB4844; AB4845; bulk exclusion + AB4822; AB4813; relabelled AB4814/AB4838; AB4837; digest stability across absolute roots (and change on a rename); custom-server skip; canonical yes on a confirming tool; positional name/aliases; AB4843 duplicate .cli.ts+.cli.tsx; one it per rejected mapInput form (ambient function/const, generator, async generator, async arrow/declaration, const-not-function, unfollowable re-export) plus an accepted-forms table. entry-shell.test.ts pins the bin template's projection import, parseInput order (defaults < mapInput < validation) and defaults: { laneKey: 'main' } only. Manifest (route-manifest-routes.test.ts) and Workbench decoder (route-manifest-client, routes-model) tests cover the mirrored fields; test-harness-manifest.test.ts pins submit as an explicit projection outside the bulk set.
  • Projection pool: harness fixture gains src/mcp/harness/tools/submit.tsx + submit.cli.tsx (value-importing lib/submit-helpers.js — the shapes a native file-URL import cannot load under Rstest) (argv positional, laneKey → --lane, tags → --tag de-duplicated by mapInput, cwd relaxed and derived, confirm: false). tests/projection/cli-dispatch-projection.test.ts invokes the operation once per surfaceinvokeMcpTool('submit', …) and invokeCli(['submit', '--lane', …, '--', …]) — and asserts equal structured results, then tests each mapping separately (rename, repeated → array, -- passthrough, derived cwd, mapInput throw → exit 2, no --yes, help with short path/spellings/Projection: line, invocation.kind === 'cli' observed by a provider). cli-dispatch-projection.test.ts 'uses cli invocation kind for bulk and explicit CLI projections while MCP remains tool' invokes one route through the bulk projection, an explicit projection, and invokeMcpTool and asserts cli/cli/tool. cli-dispatch.test.ts 'requires confirmation for a projected mutation before dispatch and strips --yes from canonical input' tests the public shell with a spy callback.
  • Integration cli-routes-build.test.ts: a temp project with src/mcp/demo/tools/submit.tsx + submit.cli.ts and a confirming purge.tsx + purge.cli.ts builds; the bin's submit --help shows the short path and renamed flags; argv round-trips through --json; cwd with a Zod .default('.') reaches mapInput as undefined (bin returns the derived root); purge without --yes exits 2 with the shared message, with --yes succeeds and no yes reaches the tool; inspect --routes shows projection. layout-build.test.ts now asserts a bulk-projected tool observes invocation: 'cli' from the bin while the layout's wrapped: route.kind stays tool and the MCP call reports tool. Full gate green after integration (1272862e5): build, typecheck, lint, lint:release, unit, route-unit, projection, integration (1131), docs site (parity + dead links).

Docs

docs/diagnostics.md, docs/entry-conventions.md; website en + zh: guide/authoring/package-entries.mdx (new "Project one tool as an idiomatic command"; bulk-projection exclusion), guide/authoring/mcp.mdx, guide/start/project-structure.mdx, guide/development/workbench.mdx, reference/configuration.mdx. Changeset agent-bundle: minor — reserving .cli.{ts,tsx} under src/mcp/** changes the meaning of a tool file that is legal today.

Consumer proof: cargo-hauler dry-run

Harness /tmp/596-dryrun/run.sh (never touches the checkout): pack agent-bundle/@agent-bundle/runtime/rsc-markdown-stream from this branch (339d65666), scratch-copy cargo-hauler, add src/mcp/hauler/tools/hauler_request.cli.ts + hauler_status.cli.ts (contents as in the design comment: status = renames only, request = argv positional + mapInput deriving cwd and splitting --after lists with cargo-hauler's parseTicketList), delete src/cli/request.tsx and src/cli/status.tsx, then inspect --routes --json, build, and the bin.

before (current cargo-hauler) after (projections, CLI routes deleted)
routes cli:request, cli:status, tool:hauler/hauler_request, tool:hauler/hauler_status tools only; no cli:request/cli:status; no misclassified …request.cli
commands request/status cli:request / cli:status tool:hauler/hauler_request · projection src/mcp/hauler/tools/hauler_request.cli.ts / tool:hauler/hauler_status · projection …/hauler_status.cli.ts
status options --lane(key lane), --status(status), --ticket(ticket), … --lane(key laneKey), --status(statuses), --ticket(tickets), --command-contains, --cwd, --limit, --session — identical spellings, canonical keys
request --help Usage: cargo-hauler request [options] <argv...>, --after … --cwd --host --session, no --yes same, plus MCP tool: hauler:hauler_request / Projection: …hauler_request.cli.ts; no --yes (confirm: false)
inspect / build 0 / 0 0 / 0 (state: ready, no AB48xx)
status --json exit 1 exit 1 — identical on both sides: the machine's running hauler daemon is 0.6.3 and the scratch CLI is 0.6.0 (restart it with hauler daemon restart); the failure is raised inside loadStatusResult, i.e. the projected command reached the canonical operation

Every flag cargo-hauler pins in README.md, src/skills/**, and tests/route-unit/cli-dispatch.test.ts (--lane, --ticket, --status, --after … -- …) stays valid; its tests/schema-compat.test.ts (asserting the CLI copy equals the protocol enum) becomes moot. The pair's remaining CLI-vs-MCP difference — follow-up wording (hauler result vs hauler_result) — is a rendering choice the tool makes from agent().invocation.kind, now 'cli' under the projection.

Deferred

Short -x aliases (the shell rejects single-dash tokens), async mapInput (rejected today with AB4844; the shell's render is synchronous), deprecating ToolConfig.exitCode, projections for resources/prompts, a positional-specific description field (flags.<key>.description applies to a key that is positional), and the Workbench operation inspector's projection view (#600).

Deslop

Deslop: gpt-5.6-sol-medium, 52 edits (cba686084: 40 comments restating code in cli-projection.ts, cli-argv.ts, cli-commands.ts, fixtures and tests; 12 type/flow simplifications — discriminated unions instead of optional-field results + !, narrowed validateFlag fields instead of casts, one flags/relaxed loop, projectedCommands carrying its narrowed projection, mapInput output kept unknown, parseCliCommandInput taking the schema instead of module.inputSchema!). Plus the owner's own pass 92346d8dd (in-place mutation in parseCliCommandInput, resolvePolicy label once, no loader dedupe).

Self-review

Reviewer ≠ author model throughout (Grok never reviewed). Owner review 08:30 → all four correctness threads fixed and resolved inline, plus (A) invocation.kind normalized (not deferred) and (B) Workbench Routes page not enriched — see the reply comment.

Pass 1 on the pre-owner-review diff (339d65666): gpt-5.6-sol-medium (compiler half) + claude-fable-5-1-thinking-max (runtime half). Findings, all fixed: defaults conflation (→ CompiledCliProjection.defaults); harness projection loading (→ registry projectionLoaders); canonical yes on a confirming tool (→ AB4845); positional name/aliases (→ AB4845); AB4843 message shapes (→ one shape); dead GeneratedCliBinSurface casts; two --yes messages (→ confirmationRequiredMessage); mapInput TypeError wording. Re-run on 920441ca0: both APPROVE, no new findings.

Pass 2 on the owner-fix diff (1272862e5, after deslop): claude-fable-5-1-thinking-high (runtime/shell/harness) + gpt-5.6-sol-medium (compiler/IR/docs).

  • Fable, Medium — parseMcpCommandInput stripped yes from every projected command, not only confirming ones, contradicting the compiler (yes reserved only when confirm) and dropping a non-confirming tool's canonical yes value. Fixed 8f035fe45: strip only when mcp.confirm; test 'hands a non-confirming projection its own canonical yes key untouched'.
  • Fable, Low — three identical openRenderedSession branches in the bin template after the kind normalization. Fixed: one call.
  • Sol, Medium — a bodyless overload signature (export function mapInput(a: A): B; with no implementation) was counted as a runtime binding. Fixed: scanRouteModuleExports adds a function declaration only when it has a body; test + AB4844 row.
  • Sol, Low — docs say the Workbench "will surface" projection details in the operation inspector while the page does not show them. Dismissed: that is the forward note the owner asked for (Workbench redesign: make dev mode an application explorer with live route rendering, MCP/hooks traces, and embedded host sessions #600); future tense, no present-tense claim.
  • Sol, Low — project-structure.mdx said bin/ appears only when src/cli/** exists. Fixed en/zh: any routed-CLI source (src/cli/**, bulk routes.mcpCommands, projection module).
  • Sol, Low — no regression test for AB4804 with routes.cli: 'conventional' beside a projection module. Fixed: test added.
  • Verified by both with no risk: single confirmation guard reachable only through runGeneratedCliEntry; kind: 'cli' in template and harness with MCP still tool and route.kind untouched; projection defaults only pre-mapInput; registry loader keys unique by construction (AB4843); deslop edits behavior-preserving; one changeset; every added file has a production importer.

Pass 3 (re-run after fixes) on 13bb3caa1: claude-fable-5-1-thinking-high — all three runtime fixes verified (non-confirming yes passthrough test uses the exact reproduced shape; one render branch; the body guard yields identical export sets for every valid TS shape, route-graph.test.ts 59/59), no concrete merge risks. gpt-5.6-sol-medium — compiler fixes and en/zh parity verified, forward-note wording confirmed future-tense, no concrete merge risks. Zero unresolved review threads. After pass 3: merged origin/main (#618, #624) with a code renumber only (AB4840–42AB4843–45, no logic change), plus the owner's 0901ae92d regression tests for the non-confirming yes passthrough; full gate green on 1297c46dc (build, typecheck, lint, lint:release, unit, route-unit, projection, integration 1132, docs site) and the owner's three test files re-run under the renumbered codes at b63d5b969. Then merged #620 (web surface; cliBinSourceInputs keeps projection sources and gains the config path main added; the non-web template hash pin in entry-shell.test.ts moves to this template's bytes) and #626; full gate green again on 3690b2aff (all pools, integration 1132, docs site); CI green on dc76050de (one packed-release.e2e flake — getByRole('heading', { name: 'Skills' }) also matches the Skills page's Source skills h2, unrelated to this PR — passed on re-run). Then merged #623 (import-only conflict in package-build.ts); full gate green on 96b1f159a.

…rojections

Add tool:harness/submit to the route-harness fixture with its CLI projection
module (parked as _submit.cli.ts until the M1 compiler classifies
projection modules; rename to submit.cli.ts at integration), the
cli-dispatch-projection projection-pool suite, and a cli-routes-build
describe that builds a temp project with src/mcp/demo/tools/submit.tsx +
submit.cli.ts and runs the generated bin and inspect --routes.

Existing pins updated for the new fixture tool: test-harness-manifest,
contract-matrix-fixtures, mcp-in-memory, cli-dispatch, packed-stdio-projection.
Discover `src/mcp/<server>/tools/<tool>.cli.{ts,tsx}` as projection
modules paired with the sibling tool route (never a route of their own;
AB4840 for orphan, misplaced, or duplicate modules; skipped silently when
the server is not generated). `extractCliProjection` validates the closed
`CliProjectionConfig` key set through the unchanged route-config grammar
and the `mapInput` export (AB4841), and binds flags, positionals, and
command segments to the tool's contract (AB4842).

`compileProjectedCliCommands` compiles one command per projected tool
under the CLI-route argv policy: `cli-argv.ts` takes a per-key override
policy (`name`, `aliases`, `description`, `default`, `required: false`)
applied inside `cliOptionFor` so kebab-case, reserved-name (`yes` when
confirming), and collision rules judge the final spellings, and reports
the canonical-required keys it relaxed. Projected tools leave the bulk
`routes.mcpCommands` projection (AB4822 when an include pattern reaches
only them); the AB4813 collision pass covers projected commands with the
projection recovery wording. A tool without a static contract is parsed
again under the `Tool route <path> (CLI projection <module>)` label so
AB4814/AB4838/AB4839 name it.

The CLI surface exists whenever cli routes, the bulk projection, or
projections exist (AB4801 third arm); `projectionSources` rides the
surface and the normalized generated bin outside the digest identity.
… and Workbench (#596)

Copy command.projection and option.aliases onto the browser catalog so a
colocated <tool>.cli.ts is visible beside usage; leave projectionSources
on the compiler surface only.
…d; document duplicate-projection AB4840 and conventional-CLI AB4804 cases
@changeset-bot

changeset-bot Bot commented Sep 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0b55643

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
agent-bundle Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-05T07:55:16.901390Z 339d656 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@pkg-pr-new

pkg-pr-new Bot commented Sep 5, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle@616
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@616
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/rsc-markdown-stream@616
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/runtime@616

commit: 0b55643

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 339d65666d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/agent-bundle/src/build/entry-shell.ts Outdated
Comment thread packages/agent-bundle/src/test/render.ts Outdated
Comment thread packages/agent-bundle/src/routes/cli-projection.ts Outdated
Comment thread packages/agent-bundle/src/cli-entry.ts Outdated
…B4842 for a confirming key yes and positional spellings; AB4840 message shape (#616)

- CompiledCliProjection.defaults: canonical key -> the projection's own
  flags.<key>.default literal (sorted, present only when declared), so the
  shell applies projection defaults alone before mapInput and a zod
  .default() stays zod's. CompiledCliOption.defaultValue keeps the effective
  default for help. Mirrored on RouteManifestCliProjection and the Workbench
  strict decoder; CliProjectionFlagDefault exported from contracts/routes.
- A tool contract key `yes` on a confirming projection is AB4842 against
  the projection module whatever the key is spelled (CliOptionPolicy
  .reservedKeys), no longer AB4814 against the tool.
- flags.<key>.name / .aliases on a key config.positionals consumes is
  AB4842; description, default, and required: false stay legal there.
- AB4840 orphan and duplicate messages use the common
  `CLI projection <module> for tool:<server>/<tool>: <detail>.` shape; the
  misplaced form is `CLI projection <module>: <detail>.`; stray duplicated
  doc comment removed. docs/diagnostics.md rows and the package-entries
  pages (en, zh) describe the actual forms.

@ScriptedAlchemy ScriptedAlchemy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direction is right: one canonical operation with CLI as a surface projection is exactly the simplification #596/#592 need. Before merge I would tighten five things:

  1. Resolve the four open correctness threads first (Zod default semantics, Rstest projection loading, ambient/generator mapInput, and confirmation enforcement in the public CLI shell). They are real behavior mismatches between the projected surface and canonical operation.
  2. Do not defer the agent().invocation.kind inconsistency if the bulk routes.mcpCommands path and explicit .cli.ts projection are both CLI surfaces. A route should not observe kind: 'tool' merely because it reached CLI through the bulk projection. Normalize this now or introduce a separate transport/projection field; otherwise application rendering can branch differently depending on which CLI projection mechanism was used.
  3. Avoid investing further in the old Workbench Routes page. Preserve the manifest/IR fields and tests, but #600 is deleting that page as a destination. Projection details belong in the selected operation's inspector in the new Application explorer. Any substantial new Routes-page-only UI should be minimized or omitted.
  4. Keep mapInput explicitly a surface adapter, not domain logic. Document that it should only reshape/default argv into canonical route input. Domain validation and behavior remain in the operation. This is important because the escape hatch is powerful enough to recreate duplicate CLI logic if left unconstrained conceptually.
  5. Consider whether .cli.ts should be described as a colocated projection module rather than a special route convention everywhere in internals. The implementation correctly excludes it from route discovery; preserve that boundary so Application IR contains one operation plus projections, not a pseudo-route hanging off MCP discovery.

The cargo-hauler dry run is strong evidence for the feature; once the above is clean this should remove a meaningful amount of consumer duplication.

ScriptedAlchemy and others added 11 commits September 5, 2026 08:38
…on.kind (#616)

Projection details belong in the #600 operation inspector, not this page.
A route observes kind: 'cli' from the generated CLI executable, including
the bulk mcpCommands projection.
…e-exported mapInput (#616)

scanRouteModuleExports records namedAmbient, namedGeneratorFunctions, and
namedUnresolved (name -> specifier) and keeps declare-d bindings out of
namedFunctions, so a projection module's mapInput compiles only when it is
a synchronous, non-generator function with a runtime binding. A relative
re-export is followed to where the function is declared; one the scan
cannot follow is rejected.
- resolvePolicy computes the fallback label once instead of repeating the
  coalesce inside overrideError
- parseCliCommandInput mutates its own copy like the generated bin it
  mirrors, instead of re-cloning for the confirmation strip and defaults
- the generated projection loaders trust the compiled manifest (one
  projection command per tool) like every sibling loader table; the
  dedupe pass is gone
- projectionCell inlines its only empty-cell text

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
@ScriptedAlchemy

Copy link
Copy Markdown
Owner Author

Owner review (08:30) — all five addressed at 1272862e5:

  1. Four correctness threads — each fixed and answered inline with the exact sites and tests, and resolved: Zod defaults (projection.defaults is the only pre-mapInput injection; canonical .default() applies in inputSchema.parse), Rstest projection loading (generated projectionLoaders registry table, version 7; .cli.tsx + .js value-import fixture), mapInput declarations (AB4841 for ambient / generator / async generator / async / unfollowable re-export / non-function, one test per form), confirmation in the public shell (parseMcpCommandInput is the single guard for bulk and projected commands, duplicates removed, shell-level spy test).
  2. agent().invocation.kind — not deferred. Every command the generated CLI executable dispatches now runs the route with kind: 'cli' (operationId = the tool id, surface = the command path), including the bulk routes.mcpCommands projection; the MCP server still passes kind: 'tool'; the worker route table (route.kind) is unchanged so layouts still wrap. Test: cli-dispatch-projection.test.ts "uses cli invocation kind for bulk and explicit CLI projections while MCP remains tool" runs one route through all three and asserts cli/cli/tool; layout-build.test.ts pins the built bin (invocation: 'cli', wrapped: 'tool'). Changeset and docs (package-entries.mdx, mcp.mdx, entry-conventions.md, en + zh) state the rule: a route observes kind: 'cli' whenever it runs from the generated CLI executable, whichever projection mechanism produced the command.
  3. Workbench Routes page — the projection note, key ↔ option table, and "Relaxed on the CLI" line and their CSS/tests are removed. The manifest/IR fields (RouteManifestCliCommand.projection, aliases, defaults) and the strict decoder + tests stay; routes-page.tsx carries a comment that command.projection is on the manifest and surfaces in the selected-operation inspector (Workbench redesign: make dev mode an application explorer with live route rendering, MCP/hooks traces, and embedded host sessions #600). workbench.mdx en/zh no longer claims the page shows it.
  4. mapInput is a surface adapter — documented in package-entries.mdx (en/zh) and docs/entry-conventions.md: it only reshapes/defaults argv into canonical input (renames, list splitting, deriving a working directory); domain validation and behaviour stay in the operation; a mapper that recreates command logic is the duplication the projection exists to remove. The compiler backs this: AB4841 for required: false/default on a canonical-required key without mapInput, and mapInput must be one synchronous runtime function.
  5. Projection module, not a route — the discovery boundary is unchanged (.cli.{ts,tsx} is excluded before identity derivation and never enters the route table); internals (src/routes/**) and every docs page now say "colocated projection module". Application IR holds one operation plus its projections.

Also per the new rule: a deslop lane (Sol, 52 edits) ran over the full diff before the reviewer pass, on top of your 92346d8dd; recorded in the PR body. Sol/Fable re-review on 1272862e5 is running now; I will fill in the Self-review section and report when green.

…less mapInput overloads; one render branch (#616)

- parseMcpCommandInput leaves a non-confirming projection's canonical yes
  key untouched (the compiler reserves yes only when confirm is true)
- scanRouteModuleExports counts a function declaration as a runtime
  binding only when it has a body, so a lone overload signature is AB4841
- the bin template's three identical openRenderedSession branches are one
- tests: non-confirming yes passthrough, bodyless overload, AB4804 with
  routes.cli conventional beside a projection module
- docs: bin/ trigger in project-structure names every routed-CLI source
…on diagnostics to AB4843–AB4845

#618 took AB4840/AB4841 for event preflight gates and declared providers, so the
CLI surface projection codes move: AB4840→AB4843 (orphan/misplaced/duplicate
module), AB4841→AB4844 (projection contract), AB4842→AB4845 (grammar binding).
cursor Bot pushed a commit that referenced this pull request Sep 5, 2026
AB4840-AB4842 are owned by the CLI surface projections of #596 (PR #616);
this branch's event-preflight export diagnostic moves to AB4850 and the
required-provider declaration diagnostic to AB4851 so the ranges stay
disjoint. Both codes are unused on main and on the #616 branch.

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
ScriptedAlchemy and others added 3 commits September 5, 2026 10:07
…rming projections (#616)

- verified on tip: parseMcpCommandInput already strips yes only when
  command.mcp.confirm is true, and AB4842 reserves the canonical yes key
  on confirming projections, so stripping is exactly framework-owned
- built executable: the projection fixture's submit tool declares an
  optional yes: z.boolean() with confirm: false; --yes reaches the tool
  through the canonical schema (and its absence stays absent)
- shell: boolean --yes passes through untouched on a non-confirming
  projection; a renamed flag keeps its canonical yes key and --yes stays
  unknown when nothing confirms
- compiler: a non-confirming projection may respell its yes key
  (flags: { yes: { name: 'assume' } }) with the canonical key intact

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
@ScriptedAlchemy ScriptedAlchemy changed the title feat(routes): CLI surface projections of MCP tools — colocated <tool>.cli.ts, mapInput, AB4840–AB4842 (#596) feat(routes): CLI surface projections of MCP tools — colocated <tool>.cli.ts, mapInput, AB4843–AB4845 (#596) Sep 5, 2026
@ScriptedAlchemy

Copy link
Copy Markdown
Owner Author

Heads-up on codes: #618 (event preflight gates) merged to main holding AB4840/AB4841, so on the merge (1297c46dc) the three CLI projection diagnostics moved to AB4843 (orphan/misplaced/duplicate module), AB4844 (projection contract, incl. the mapInput forms), AB4845 (grammar binding, incl. canonical yes on a confirming tool and positional name/aliases). Code, tests, docs/diagnostics.md, en/zh pages, changeset, PR title and body are renumbered; the resolved threads above and the design comment on #596 use the old numbers for the same diagnostics. Your 0901ae92d regression tests are merged and green under the new codes. Head b63d5b969; full gate green; waiting on CI.

@ScriptedAlchemy
ScriptedAlchemy enabled auto-merge (squash) September 5, 2026 16:20
@ScriptedAlchemy
ScriptedAlchemy merged commit efdec6b into main Sep 5, 2026
16 checks passed
@ScriptedAlchemy
ScriptedAlchemy deleted the feat/596-cli-surface-projection branch September 5, 2026 16:27
ScriptedAlchemy added a commit that referenced this pull request Sep 5, 2026
…ative-manifest; serialize CompiledCliProjection and option aliases into routes.cli.commands[]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Route projections: let one operation drive MCP + idiomatic CLI without duplicate route modules

1 participant